﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Diagnostics;
using System.IO;
using Bio;
using System.Collections.ObjectModel;
using Bio.IO.FastA;
using Bio.Web.Blast;
using Bio.Web;
using System.Threading.Tasks;

namespace CandyBlast
{



    public class Blaster
    {

#if DEBUG
        private const string BLAST_RESULT_FOLDER = "C:\\BLAST\\results";
        private const string BLAST_EXECUTABLE = "C:\\blast\\bin\\blastall.exe";
        private const string BLAST_WORKINGDIR = "C:\\blast\\bin\\";
#else
        private const string BLAST_RESULT_FOLDER = "E:\\BLAST\\results";
        private const string BLAST_EXECUTABLE = "E:\\blast\\bin\\blastall.exe";
        private const string BLAST_WORKINGDIR = "E:\\blast\\bin\\"; 
#endif

        public List<BlastJob> BlastFishingDbs(int projectId, int familyId, Guid userUid)
        {
            List<BlastJob> jobs = new List<BlastJob>();

            var sequences = GetFamilySequences(familyId, projectId);
            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
            {
                foreach (var db in context.Databases.Where(x => x.IsFishingDB && x.FishingEnabled == true))
                {
                    BlastJob j = new BlastJob();
                    //j.UId = Guid.NewGuid();
                    j.UserUId = userUid;
                    j.ErrorMessage = sequences.Count > 0 ? "" : "Family doesn't contain any genes to BLAST.";
                    j.Status = sequences.Count > 0 ? (int)RequestStatus.PENDING : (int)RequestStatus.REQUEST_FAILED;
                    j.Task = (int)BlastTask.blast_fishing_dbs;
                    j.TimePoint = DateTime.Now;
                    j.DatabaseId = db.Id;
                    j.LocalDatabaseName = db.LocalDBName;
                    j.DatabaseCollection = db.Collection;
                    j.FamilyId = familyId;
                    j.Name = "BLAST_" + db.DisplayName;
                    //j.Project = projectName;
                    context.BlastJobs.InsertOnSubmit(j);
                    context.SubmitChanges();
                    jobs.Add(j);
                }

                context.SubmitChanges();
            }


            if (sequences.Count < 1) //if there are no sequences then we are done here
            {
                return jobs;
            }


            Thread th = new Thread(delegate() //lets run the blasts now
            {
                CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId));
                foreach (var item in jobs)
                {
                    BlastJob job = context.BlastJobs.Single(x => x.Id == item.Id);
                    BlastFishingDB(context, projectId, job, sequences);
                }
            });
            th.IsBackground = true;
            th.Start();
            return jobs;
        }


        private void BlastFishingDB(CandyDBDataContext projectContext, int projectId, BlastJob job, ICollection<ISequence> sequences)
        {
            //   CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(project));
            try
            {
                // BlastJob job = context.BlastJobs.Single(x => x.UId == jobUid);
                Process process = new Process();
                string tempFileName = Path.GetTempFileName();
                string tempout;


                //File.WriteAllText(tempFileName, fasta);
                FastAFormatter formatter = new FastAFormatter(tempFileName);
                formatter.Write(sequences);
                formatter.Flush();
                formatter.Close();


                ProcessStartInfo info;

                tempout = Path.Combine(BLAST_RESULT_FOLDER, job.Id.ToString("00000000") + ".xml");
                info = new ProcessStartInfo(BLAST_EXECUTABLE, string.Format("-p blastp -d {2} -i {0} -o {1} -m 7 -e 0.1e-5 -a 4", tempFileName, tempout, job.LocalDatabaseName));
                info.WorkingDirectory = Path.Combine(BLAST_WORKINGDIR, job.DatabaseCollection);
                info.RedirectStandardError = true;
                info.CreateNoWindow = false;
                info.WindowStyle = ProcessWindowStyle.Hidden;
                info.UseShellExecute = false;
                process.StartInfo = info;
                job.Parameter = info.FileName + " " + info.Arguments;
                projectContext.SubmitChanges();

                process.Start();
                job.Status = (int)RequestStatus.RUNNING;
                projectContext.SubmitChanges();
                string errorStream = process.StandardError.ReadToEnd();
                process.WaitForExit((int)Math.Floor(TimeSpan.FromMinutes(30).TotalMilliseconds));

                if (process.ExitCode == 0 && process.HasExited)
                {
                    try
                    {
                        //job.Status = (int)JobStatus.DONE;
                        string importError;
                        NcbiBlastImporter importer = new NcbiBlastImporter();
                        if (importer.Import(job.Id, File.ReadAllText(tempout), projectId, out importError) == false)
                        {
                            job.Status = (int)RequestStatus.ERROR;
                            job.ErrorMessage = "Job failed during result parsing with exception: \r\n" + importError;
                            projectContext.SubmitChanges();
                        }
                        else
                        {
                            job.Status = (int)RequestStatus.RESULTS;
                            projectContext.SubmitChanges();
                        }
#if !DEBUG
                     //   File.Delete(tempFileName);
#endif
                        //  ProcessResults(job);

                        // job.Message = "Job successful finished";
                    }
                    catch (Exception ex)
                    {
                        job.Status = (int)RequestStatus.ERROR;
                        job.ErrorMessage = "Job failed during result parsing with exception: \r\n" + ex.Message;
                    }
                    finally
                    {
                        projectContext.SubmitChanges();

                    }

                }
                else
                {
                    if (process.HasExited == false)
                    {
                        process.Kill();
                        job.Status = (int)RequestStatus.ERROR;
                        job.ErrorMessage = "Job failed because the process timed out " + process.ExitCode + "\r\n" + info.FileName + " " + info.Arguments + "\r\n" + errorStream;
                        projectContext.SubmitChanges();

                    }
                    else
                    {
                        job.Status = (int)RequestStatus.ERROR;
                        job.ErrorMessage = "Job failed during BLAST execution with exit code " + process.ExitCode + "\r\n" + info.FileName + " " + info.Arguments + "\r\n" + errorStream;
                        projectContext.SubmitChanges();
                    }
                }
              //  File.Delete(tempFileName);

            }
            catch (Exception exception)
            {
                job.Status = (int)RequestStatus.ERROR;
                job.ErrorMessage = "Job failed during job preparation with exception: \r\n" + exception.Message + "\r\n" + exception.StackTrace;
                projectContext.SubmitChanges();
            }

        }

        private ICollection<ISequence> GetFamilySequences(int familyId, int projectId)
        {
            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
            {
                var genes = from x in context.Genes
                            where x.FamilyId == familyId
                            where x.DatabaseId.HasValue == false || x.Database.IsFishingDB == false
                            select x;
                StringBuilder sb = new StringBuilder();
                Collection<ISequence> sequences = new Collection<ISequence>();

                foreach (Gene gene in genes)
                {
                    if (!string.IsNullOrEmpty(gene.PROTSequence))
                    {
                        Sequence s = new Sequence(Alphabets.AmbiguousProtein, gene.PROTSequence.Replace("\r", "").Replace("\n", ""));
                        s.ID = gene.Id.ToString("00000000");
                        sequences.Add(s);
                        //sb.AppendFormat(">{0}\r\n", gene.UId);
                        //sb.AppendFormat("{0}\r\n", ToFasta(gene.PROTSequence));
                    }
                }

                return sequences;
                // return new ASCIIEncoding().GetBytes(sb.ToString());
            }
        }

        public BlastJob BlastEbi(int projectId, int geneId, string localDatabseName, Guid userUid, string userEmail)
        {
            ISequence sequence = null;
            BlastJob j;
            string jobId = null;
            EbiWuBlastHandler wuBlast = null;
            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
            {
                Database db = context.Databases.Single(x => x.LocalDBName == localDatabseName);
                var gene = context.Genes.SingleOrDefault(x => x.Id == geneId);
                bool hashasValidProtSequence = true;
                if (gene == null)
                {
                    hashasValidProtSequence = false;
                }
                else
                {
                    try
                    {
                        sequence = new Sequence(Alphabets.AmbiguousProtein, gene.PROTSequence.Replace("\r", "").Replace("\n", ""));
                        sequence.ID = gene.Id.ToString("00000000");
                    }
                    catch
                    {
                        hashasValidProtSequence = false;
                    }
                }
                j = new BlastJob();
                // j.UId = Guid.NewGuid();

                j.UserUId = userUid;
                j.ErrorMessage = hashasValidProtSequence ? "" : "Gene not found or doesn't have a valid protein seqence to BLAST.";
                j.Status = hashasValidProtSequence ? (int)RequestStatus.PENDING : (int)RequestStatus.REQUEST_FAILED;
                j.Task = (int)BlastTask.find_ortholog_genes;
                j.TimePoint = DateTime.Now;
                j.DatabaseId = db.Id; //TODO: Database suche as SwissProt shoud be preinstalled
                j.LocalDatabaseName = localDatabseName;
                j.DatabaseCollection = db.Collection;
                j.GeneId = geneId;
                j.FamilyId = gene.FamilyId;
                j.Name = "BLAST_" + db.DisplayName;
                //j.Project = projectName;
                context.BlastJobs.InsertOnSubmit(j);
                context.SubmitChanges();


                if (j.Status == (int)RequestStatus.REQUEST_FAILED)
                    return j;


                ConfigParameters conf = new ConfigParameters();

                wuBlast = new EbiWuBlastHandler();

                wuBlast.Configuration.EmailAddress = userEmail;
                //            ip.database = new string[] { "uniprotkb_swissprot" };
                //            ip.program = "blastp";
                //            ip.stype = "protein";
                //            ip.sequence = ">bla\r\n" + gene.PROTSequence.Replace("\r", "").Replace("\n", "").Replace(" ", "");
                BlastParameters p = new BlastParameters();
                p.Add("Program", "blastp");
                p.Add("Database", localDatabseName);
                p.Add("SequenceType", "protein");
                p.Add("Email", userEmail);
                j.JobId = wuBlast.SubmitRequest(sequence, p);
                jobId = j.JobId;
                context.SubmitChanges();
                WatchEbiJobAsync(projectId, j.Id, jobId, p);
            }

            return j;

        }

        public void WatchEbiJobAsync(int projectId, int jobId, string ebijobId, BlastParameters parameters)
        {
            Task t = new Task(() =>
                {
                    Thread.Sleep(5000);
                    EbiWuBlastHandler wuBlast = new EbiWuBlastHandler();
                    ServiceRequestInformation info = null;

                    int maxAttempts = 40;
                    int attempt = 1;

                    bool failedOrDone = false;
                    while (attempt <= maxAttempts && !failedOrDone)
                    {
                        Thread.Sleep(5000);
                        info = wuBlast.GetRequestStatus(ebijobId); //try again
                        if (info.Status == ServiceRequestStatus.Ready || info.Status == ServiceRequestStatus.Error || info.Status == ServiceRequestStatus.Canceled)
                            failedOrDone = true;
                        if (info.StatusInformation.ToLower().Contains("running"))
                        {
                            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
                            {
                                BlastJob bj = context.BlastJobs.Single(x => x.Id == jobId);
                                bj.Status = (int)RequestStatus.RUNNING;
                                context.SubmitChanges();
                            }
                        }
                        attempt++;
                    }

                    //process status
                    using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
                    {
                        BlastJob bj = context.BlastJobs.Single(x => x.Id == jobId);
                        if (info.Status == ServiceRequestStatus.Ready)
                        {
                            bj.Status = (int)RequestStatus.RUNNING;
                            context.SubmitChanges();
                        }
                        else if (info.Status == ServiceRequestStatus.Error)
                        {
                            bj.Status = (int)RequestStatus.ERROR;
                            bj.ErrorMessage += info.StatusInformation;
                            context.SubmitChanges();
                        }
                        else if (info.Status == ServiceRequestStatus.Canceled)
                        {
                            bj.Status = (int)RequestStatus.ERROR;
                            bj.ErrorMessage = "Job failed or time out. Last job status was '" + info.Status.ToString() + "'";
                            context.SubmitChanges();
                        }
                    }

                    //porcess results
                    IList<BlastResult> results;
                    if (info.Status == ServiceRequestStatus.Ready)
                    {

                        try
                        {
                            results = wuBlast.FetchResultsSync(ebijobId, parameters) as List<BlastResult>;
                        }
                        catch (Exception ex)
                        {
                            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
                            {
                                BlastJob bj = context.BlastJobs.Single(x => x.Id == jobId);
                                bj.Status = (int)RequestStatus.ERROR;
                                bj.ErrorMessage = "Fetching results failed wit error: " + ex.Message + "'";
                                context.SubmitChanges();
                            }
                            return;
                        }
                        using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
                        {
                            BlastJob job = context.BlastJobs.Single(x => x.Id == jobId);
                            try
                            {
                                //job.Status = (int)JobStatus.DONE;
                                string importError;
                                WuBlastHandlerImporter importer = new WuBlastHandlerImporter();
                                if (importer.Import(jobId, job.GeneId.Value, results, projectId, "SP:", out importError) == false)
                                {
                                    job.Status = (int)RequestStatus.ERROR;
                                    job.ErrorMessage = "Job failed during result parsing with exception: \r\n" + importError;
                                    context.SubmitChanges();
                                }
                                else
                                {
                                    job.Status = (int)RequestStatus.RESULTS;
                                    context.SubmitChanges();
                                }
                            }
                            catch (Exception ex)
                            {
                                job.Status = (int)RequestStatus.ERROR;
                                job.ErrorMessage = "Job failed during result parsing with exception: \r\n" + ex.Message;
                            }
                            finally
                            {
                                context.SubmitChanges();

                            }
                        }
                    }


                });
            t.Start();
        }

        public void RecheckOldJobs(int projectId)
        {
            using (CandyDBDataContext context = new CandyDBDataContext(ProjectConnector.GetCandyConnectionString(projectId)))
            {
                var jobs = context.BlastJobs.Where(x => (x.Status == (int)RequestStatus.PENDING || x.Status == (int)RequestStatus.RUNNING) && x.TimePoint <= DateTime.Now.AddMinutes(-60));
                foreach (var item in jobs)
                {
                    item.Status = (int)RequestStatus.ERROR;
                    item.ErrorMessage = "Job failed because it exceeded the alloted time.";
                }

                context.SubmitChanges();

            }

        }
    }
}

